package com.ly.recommend;

import org.springframework.stereotype.Component;

import java.util.*;

/**
 * @author : 黑渊白花
 * @ClassName ItemCF
 * @date : 2025/5/10 20:11
 * @Description
 */
@Component
public class ItemCF {

    /**
     * 基于物品相似度的协同过滤推荐算法主方法。
     * 该方法根据用户对物品的评分数据，针对指定用户生成推荐物品列表。
     *
     * @param ratings 用户评分数据，外层键为用户ID，内层键为物品ID，值为对应评分。
     * @param userId  目标用户的ID，用于生成针对该用户的推荐列表。
     * @param numRecommendations 期望生成的推荐物品数量，决定了最终返回推荐列表的长度。
     * @return 返回一个包含推荐物品ID的列表，列表长度由numRecommendations决定或者受限于可推荐物品数量（取较小值）。
     */
    public static List<Integer> recommend(Map<Integer, Map<Integer, Double>> ratings, int userId, int numRecommendations) {
        // 检查目标用户是否存在
        if (!ratings.containsKey(userId)) {
            return new ArrayList<>();
        }

        Map<Integer, Map<Integer, Double>> itemSimilarities = calculateItemSimilarities(ratings);
        Map<Integer, Double> userRatings = ratings.get(userId);
        List<Integer> ratedItems = new ArrayList<>(userRatings.keySet());
        Set<Integer> processedItems = new HashSet<>();

        // 使用优先队列维护前numRecommendations个预测评分最高的物品
        PriorityQueue<Map.Entry<Integer, Double>> priorityQueue = new PriorityQueue<>(
                numRecommendations, (a, b) -> b.getValue().compareTo(a.getValue()));

        // 遍历所有用户及其评分数据，针对目标用户未评分的物品进行评分预测
        for (Map.Entry<Integer, Map<Integer, Double>> entry : ratings.entrySet()) {
            Map<Integer, Double> otherUserRatings = entry.getValue();
            int otherUserId = entry.getKey();
            if (otherUserId == userId) continue;

            for (Integer itemId : otherUserRatings.keySet()) {
                if (!ratedItems.contains(itemId) && !processedItems.contains(itemId)) {
                    double predictedRating = predictRating(userRatings, itemSimilarities, itemId, otherUserRatings);
                    priorityQueue.offer(new AbstractMap.SimpleEntry<>(itemId, predictedRating));
                    if (priorityQueue.size() > numRecommendations) {
                        priorityQueue.poll();
                    }
                    processedItems.add(itemId);
                }
            }
        }

        // 将优先队列中的物品ID按预测评分从高到低排序
        List<Integer> recommendedItems = new ArrayList<>();
        while (!priorityQueue.isEmpty()) {
            recommendedItems.add(0, priorityQueue.poll().getKey());
        }

        return recommendedItems;
    }

    /**
     * 计算物品之间相似度的方法，采用余弦相似度计算。
     * 该方法通过分析所有用户对物品的评分数据，构建出物品之间的相似度矩阵，
     * 矩阵中记录了每对物品之间的相似度得分，反映了物品在用户评分行为上的相似程度。
     *
     * @param ratings 用户评分数据，用于分析物品之间的相似程度。
     * @return 返回一个表示物品相似度的矩阵，外层键为物品ID，内层键为与之对比的物品ID，值为相似度得分。
     */
    private static Map<Integer, Map<Integer, Double>> calculateItemSimilarities(Map<Integer, Map<Integer, Double>> ratings) {
        Map<Integer, Map<Integer, Double>> itemSimilarities = new HashMap<>();
        Set<Integer> allItemIds = new HashSet<>();

        // 收集所有物品ID
        for (Map<Integer, Double> userRatings : ratings.values()) {
            allItemIds.addAll(userRatings.keySet());
        }
        List<Integer> itemIds = new ArrayList<>(allItemIds);

        // 只计算矩阵的上三角部分，减少冗余计算
        for (int i = 0; i < itemIds.size(); i++) {
            int item1Id = itemIds.get(i);
            Map<Integer, Double> item1Ratings = getItemRatings(ratings, item1Id);
            Map<Integer, Double> similaritiesForItem1 = new HashMap<>();

            for (int j = i + 1; j < itemIds.size(); j++) {
                int item2Id = itemIds.get(j);
                Map<Integer, Double> item2Ratings = getItemRatings(ratings, item2Id);

                double similarity = calculateItemSimilarity(item1Ratings, item2Ratings);
                similaritiesForItem1.put(item2Id, similarity);

                // 更新对称部分的相似度
                if (!itemSimilarities.containsKey(item2Id)) {
                    itemSimilarities.put(item2Id, new HashMap<>());
                }
                itemSimilarities.get(item2Id).put(item1Id, similarity);
            }

            itemSimilarities.put(item1Id, similaritiesForItem1);
        }

        return itemSimilarities;
    }

    private static Map<Integer, Double> getItemRatings(Map<Integer, Map<Integer, Double>> ratings, int itemId) {
        Map<Integer, Double> itemRatings = new HashMap<>();
        for (Map.Entry<Integer, Map<Integer, Double>> entry : ratings.entrySet()) {
            int userId = entry.getKey();
            Map<Integer, Double> userRatings = entry.getValue();
            if (userRatings.containsKey(itemId)) {
                itemRatings.put(userId, userRatings.get(itemId));
            }
        }
        return itemRatings;
    }

    /**
     * 计算两个物品之间的相似度（采用余弦相似度）。
     * 通过分析两个物品各自对应的用户评分数据，按照余弦相似度的计算公式，
     * 得出反映它们在用户评分行为上相似程度的得分，范围在0到1之间（包含0和1），越接近1表示相似度越高。
     *
     * @param item1Ratings 第一个物品的用户评分数据，键为用户ID，值为对应评分。
     * @param item2Ratings 第二个物品的用户评分数据，结构与item1Ratings相同，用于对比计算相似度。
     * @return 返回一个双精度浮点数，表示两个物品之间的相似度得分，范围在0到1之间（包含0和1），值越接近1表示相似度越高。
     */
    private static double calculateItemSimilarity(Map<Integer, Double> item1Ratings, Map<Integer, Double> item2Ratings) {
        double dotProduct = 0.0;
        double item1Magnitude = 0.0;
        double item2Magnitude = 0.0;

        for (Map.Entry<Integer, Double> entry : item1Ratings.entrySet()) {
            int userId = entry.getKey();
            if (item2Ratings.containsKey(userId)) {
                dotProduct += entry.getValue() * item2Ratings.get(userId);
                item1Magnitude += Math.pow(entry.getValue(), 2);
                item2Magnitude += Math.pow(item2Ratings.get(userId), 2);
            }
        }

        if (item1Magnitude == 0 || item2Magnitude == 0) {
            return 0;
        }

        return dotProduct / (Math.sqrt(item1Magnitude) * Math.sqrt(item2Magnitude));
    }

    /**
     * 预测用户对某个未评分物品的评分。
     * 通过结合用户已评分物品的评分、物品之间的相似度矩阵以及其他用户对该未评分物品的评分等信息，
     * 按照特定的计算公式来预测目标用户对该未评分物品的评分值。
     *
     * @param userRatings 用户已有的评分数据，用于结合物品相似度来预测评分。
     * @param itemSimilarities 物品相似度矩阵，提供物品间的相似程度信息。
     * @param itemId 待预测评分的物品ID。
     * @param otherUserRatings 其他用户的评分数据，用于参考计算预测评分。
     * @return 返回预测的用户对该物品的评分值，为双精度浮点数。
     */
    private static double predictRating(Map<Integer, Double> userRatings,
                                        Map<Integer, Map<Integer, Double>> itemSimilarities,
                                        int itemId,
                                        Map<Integer, Double> otherUserRatings) {
        double numerator = 0.0;
        double denominator = 0.0;

        for (Map.Entry<Integer, Double> ratingEntry : userRatings.entrySet()) {
            int ratedItemId = ratingEntry.getKey();
            double rating = ratingEntry.getValue();

            // 检查itemSimilarities和相似度是否存在
            Map<Integer, Double> similaritiesForRatedItem = itemSimilarities.get(ratedItemId);
            if (similaritiesForRatedItem != null) {
                Double similarity = similaritiesForRatedItem.get(itemId);
                if (similarity != null) {
                    numerator += rating * similarity;
                    denominator += Math.abs(similarity);
                }
            }
        }

        if (denominator == 0) {
            return 0;
        }

        return numerator / denominator;
    }
}